﻿using System;
using System.Collections.Generic;
using PostSharp.Aspects;
using Sapit.Attributes.Interfaces;
using Sapit.Helpers;

namespace Sapit.PostSharp.Attributes
{
    /// <summary>
    /// Apply this attribute to methods where calls should be throttled when response times exceeds a certain threshold.
    /// </summary>
    [Serializable]
    public class ResponseTimeThrottleAttribute : MethodInterceptionAspect, IResponseTimeThrottle
    {
        /// <summary>
        /// The threshold to trigger call throttling
        /// </summary>
        public double ThrottleThresholdMilliseconds { get; set; }
        /// <summary>
        /// The number of calls to skip when throttling
        /// </summary>
        public int SkipCallsWhenThrottling { get; set; }
        /// <summary>
        /// Defines the behavior when throttling, return a default value or throw an exception
        /// </summary>
        public Behavior BehaviorWhenThrottling { get; set; }
        /// <summary>
        /// The value to return when skipping calls
        /// </summary>
        public object ThrottleReturnValue { get; set; }

        [NonSerialized]
        private static Dictionary<Invocation, int> throttledInvocations = new Dictionary<Invocation, int>();

        public override void OnInvoke(MethodInterceptionArgs args)
        {
            Invocation inv = new Invocation() { InvocationTarget = args.Instance, Method = args.Method };

            if (IsThisInvocationThrottled(inv))
            {
                DecrementThrottleCounterForThisInvocation(inv);

                switch (BehaviorWhenThrottling)
                {
                    case Behavior.Undefined:
                    case Behavior.ReturnDefaultValue:
                        args.ReturnValue = ThrottleReturnValue;
                        break;
                    case Behavior.ThrowException:
                        throw new InvalidOperationException(string.Format("{0}", ThrottleReturnValue));
                    default:
                        args.ReturnValue = ThrottleReturnValue;
                        break;
                }
            }
            else
            {
                UnThrottleThisInvocation(inv);
                double responseTime = InvokeMethodAndRecordResponseTime(args);
                if (responseTime > ThrottleThresholdMilliseconds)
                {
                    ThrottleThisInvocation(inv, SkipCallsWhenThrottling);
                }
            }
        }

        private bool IsThisInvocationThrottled(Invocation invocation)
        {
            return
                throttledInvocations.ContainsKey(invocation) &&
                throttledInvocations[invocation] > 0;
        }

        private void DecrementThrottleCounterForThisInvocation(Invocation invocation)
        {
            if (throttledInvocations.ContainsKey(invocation))
            {
                throttledInvocations[invocation] -= 1;
            }
        }

        private void UnThrottleThisInvocation(Invocation invocation)
        {
            if (throttledInvocations.ContainsKey(invocation))
            {
                throttledInvocations.Remove(invocation);
            }
        }

        private double InvokeMethodAndRecordResponseTime(MethodInterceptionArgs args)
        {
            DateTime beforeInvocation = DateTime.Now;
            args.Proceed();
            DateTime afterInvocation = DateTime.Now;
            return (afterInvocation - beforeInvocation).TotalMilliseconds;
        }

        private void ThrottleThisInvocation(Invocation invocation, int callsToSkip)
        {
            if (throttledInvocations.ContainsKey(invocation))
            {
                throttledInvocations.Remove(invocation);
            }
            throttledInvocations.Add(invocation, callsToSkip);
        }
    }
}
